using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Xml.Schema;
using NVelocity;
using NVelocity.App;
using NVelocity.Context;
using uTILLIty.Rave.Contract.Configuration;
using uTILLIty.Rave.Contract.Exception;
using uTILLIty.Rave.Contract.Job;
using uTILLIty.Rave.Contract.StorageAdapter;
using uTILLIty.Rave.Core.Configuration;
using uTILLIty.Rave.Core.Strategy;

namespace uTILLIty.Rave.Core.Manager
{
	/// <summary>
	/// A manager handling <see cref="Configuration"/>-specific use-cases
	/// </summary>
	public class ConfigurationManager
	{
		private readonly Func<string, VelocityEngine> _velocityEngineFactory;
    private readonly Func<string, IStorageAdapter> _storageAdapterFactory;
    private readonly Func<string, IConfigurationValidatorStrategy> _configurationValidatorStrategyFactory;
    private readonly Func<string, IConfigurationApplicatorStrategy> _configurationApplicatorStrategyFactory;
    private readonly Func<string, IConfigurationConstructorStrategy> _configurationConstructorStrategyFactory;
    private readonly Func<Contract.Configuration.Configuration, ApplicatorContext> _applicatorContextFactory;

		private readonly IEnumerable<XmlSchema> _validationSchemas;

		private readonly CsvManager _csvManager;

		// ReSharper disable CSharpWarnings::CS1591
		/// <summary>
		/// An IoC constructor for creating a new instance of this manager
		/// </summary>
		/// <param name="velocityEngineFactory">required</param>
		/// <param name="storageAdapterFactory">required</param>
		/// <param name="configurationValidatorStrategyFactory">required</param>
		/// <param name="configurationApplicatorStrategyFactory">required</param>
		/// <param name="validationSchemas">optional</param>
		/// <param name="applicatorContextFactory">required</param>
		/// <param name="configurationConstructorStrategyFactory">required for <see cref="BuildFromDirectory"/></param>
		/// <param name="csvManager">required for <see cref="LoadAndParse"/>, if using parameters in the job</param>
		public ConfigurationManager(Func<string, VelocityEngine> velocityEngineFactory, Func<string, IStorageAdapter> storageAdapterFactory,
      Func<string, IConfigurationValidatorStrategy> configurationValidatorStrategyFactory, Func<string, IConfigurationApplicatorStrategy> configurationApplicatorStrategyFactory, 
      IEnumerable<XmlSchema> validationSchemas,Func<Contract.Configuration.Configuration, ApplicatorContext> applicatorContextFactory, 
      Func<string, IConfigurationConstructorStrategy> configurationConstructorStrategyFactory, CsvManager csvManager)
		{
			_velocityEngineFactory = velocityEngineFactory;
			_storageAdapterFactory = storageAdapterFactory;
      _configurationValidatorStrategyFactory = configurationValidatorStrategyFactory;
      _configurationApplicatorStrategyFactory = configurationApplicatorStrategyFactory;
			_validationSchemas = validationSchemas;
			_applicatorContextFactory = applicatorContextFactory;
      _configurationConstructorStrategyFactory = configurationConstructorStrategyFactory;
			_csvManager = csvManager;
		}
		// ReSharper restore CSharpWarnings::CS1591

		/// <summary>
		/// Raised after the merge, before the XML is deserialized. During this event you can see and modify the XML
		/// </summary>
		/// <remarks>
		/// If the event-handler returns NULL, the original parsed XML information is used instead
		/// </remarks>
		public event Func<Job, string, string> OnAfterMerge;

		/// <summary>
		/// Loads and parses the configuration contained in the specified <paramref name="job"/>
		/// </summary>
		public Contract.Configuration.Configuration LoadAndParse(Job job)
		{
			var velocityContext = new VelocityContext();
			PrepareDefaultVelocityContext(velocityContext, job.GetEncoding());
			if (job.MergeVariables != null)
			{
				foreach (var env in job.MergeVariables)
				{
					SetValue(env.Key, env.Value, velocityContext, job.GetEncoding());
				}
			}

			Template template = GetTemplate(new FileInfo(job.Template), job.TemplateEncoding);
			string output = Merge(template, velocityContext);

			if (OnAfterMerge != null)
				output = OnAfterMerge.Invoke(job, output) ?? output;

			if (job.SaveMergedOutput)
			{
				var filename = Path.ChangeExtension(job.AddJobIdToFilename(job.JobSource), ".merged.xml");
				File.WriteAllText(filename, output);
			}

			Contract.Configuration.Configuration configuration = GetConfiguration(output, job.ValidateAfterMerge, job.TemplateEncoding);

			if (job.ValidateAfterMerge)
			{
				ApplicatorContext context = CreateApplicatorContext(job, configuration, true);
				context.StorageAdapter = GetAdapter(context);
				context.StorageAdapter.TraceSource = job.TraceSource;
				EnsureValid(context);
				job.TraceSource.Flush();
			}

			return configuration;
		}

		/// <summary>
		/// Adds a value to the VelocityContext
		/// </summary>
		/// <param name="key">the key to add</param>
		/// <param name="value">the value of the key to add</param>
		/// <param name="velocityContext">the context to add the value to</param>
		/// <param name="encoding">The XML encoding for the value</param>
		protected virtual void SetValue(string key, object value, VelocityContext velocityContext, Encoding encoding)
		{
			if (value is string)
				velocityContext.Put(key, _csvManager.XmlEscape((string)value, encoding));
			else
				velocityContext.Put(key, value);
		}

		/// <summary>
		/// Adds default context variables
		/// </summary>
		/// <remarks>
		/// <list type="table">
		///		<listheader>
		///			<term>Key</term>
		///			<description>Description</description>
		///		</listheader>
		///		<item>
		///			<term>Now</term>
		///			<description>The current date/time as a DateTime object</description>
		///		</item>
		/// </list>
		/// </remarks>
		///<param name="velocityContext">the context to add the values to</param>
		///<param name="encoding">the encoding to use for parameter values</param>
		protected virtual void PrepareDefaultVelocityContext(VelocityContext velocityContext, Encoding encoding)
		{
			var user = WindowsIdentity.GetCurrent();
			var name = user != null ? user.Name.Split('\\') : new[] { "UNKNOWN", "UNKNOWN" };
			var env = Environment.GetEnvironmentVariables();
			foreach (string key in env.Keys)
			{
				SetValue("ENV_" + key.ToUpper(), env[key], velocityContext, encoding);
			}
			SetValue("Now", DateTime.Now, velocityContext, encoding);
			SetValue("CurrentUser", name[1], velocityContext, encoding);
			SetValue("CurrentDomain", name[0], velocityContext, encoding);
			SetValue("CurrentUserSID", user != null && user.User != null
				? user.User.Value
				: "UNKNOWN", velocityContext, encoding);
		}

		/// <summary>
		/// Applies the <paramref name="configuration"/> to the target-storage
		/// </summary>
		/// <param name="job">The job to process (needed for preparing the logfile)</param>
		/// <param name="configuration">The configuration to process/apply</param>
		/// <param name="simulateOnly">wether to only log what would be done, or actually apply any actions</param>
		public void Apply(Job job, Contract.Configuration.Configuration configuration, bool simulateOnly)
		{
			if (configuration == null)
				throw new ArgumentNullException("configuration");

			ApplicatorContext context = CreateApplicatorContext(job, configuration, simulateOnly);
			context.StorageAdapter = GetAdapter(context);
			context.StorageAdapter.TraceSource = job.TraceSource;
			EnsureValid(context);

			/* configurationApplicatorStrategy is factory-scoped for multi-threading, make sure,
			 * it's only invoked once per run, as the context (supplied during Apply call below) 
			 * is not available otherwise! */
		  var configurationApplicatorStrategy = _configurationApplicatorStrategyFactory.Invoke(configuration.TargetSystem);
			context.StorageAdapter.MemberResolver = configurationApplicatorStrategy;
      configurationApplicatorStrategy.Apply(context);
			job.TraceSource.Flush();
		}

		#region Helpers
		/// <summary>
		/// Returns the corresponding <see cref="IStorageAdapter">adapter</see>
		/// </summary>
		public virtual IStorageAdapter GetAdapter(ApplicatorContext context)
		{
      //throws an exception, if not registered
			IStorageAdapter storageAdapter = _storageAdapterFactory.Invoke(context.TargetSystem);
			return storageAdapter;
		}

		/// <summary>
		/// Creates the <see cref="ApplicatorContext">context</see> for processing the supplied <paramref name="configuration"/>
		/// </summary>
		public virtual ApplicatorContext CreateApplicatorContext(Job job, Contract.Configuration.Configuration configuration, bool simulateOnly)
		{
			var context = _applicatorContextFactory.Invoke(configuration);
			context.SimulateOnly = simulateOnly;
			context.Job = job;
			return context;
		}
		#endregion

		#region Validation

		/// <summary>
		/// Ensures that the configuration is valid
		/// </summary>
		/// <exception cref="ConfigurationErrorException">Thrown, if any errors where found</exception>
		/// <param name="context">the context to validate</param>
		protected virtual void EnsureValid(ApplicatorContext context)
		{
      var strategy = _configurationValidatorStrategyFactory.Invoke(context.Configuration.TargetSystem);
      var errors = strategy.Validate(context);

			if (errors.Length > 0)
			{
				throw new ConfigurationErrorException(Properties.ExceptionMessages.ValidationErrors, string.Join("", errors.Select(e => "\n - " + e)));
			}
		}

		#endregion

		#region Load Configuration

		/// <summary>
		/// Parses and deserializes the supplied <paramref name="configDataAsString"/>
		/// </summary>
		/// <param name="configDataAsString">the XML data representing the configuration to be parsed</param>
		/// <param name="withValidation">wether to validate the XML before deserializing it</param>
		/// <param name="encoding">the encoding of the XML file</param>
		/// <returns>A new instance of the <see cref="Configuration"/>, if successful</returns>
		protected virtual Contract.Configuration.Configuration GetConfiguration(string configDataAsString, bool withValidation, string encoding)
		{
			using (var configData = new MemoryStream(configDataAsString.Length))
			{
				using (var writer = new StreamWriter(configData, Encoding.GetEncoding(encoding)))
				{
					writer.Write(configDataAsString);
					writer.Flush();
					configData.Seek(0, SeekOrigin.Begin);
					Contract.Configuration.Configuration configuration = null;
					// ReSharper disable ConditionIsAlwaysTrueOrFalse
					// ReSharper disable ExpressionIsAlwaysNull
					configuration = configuration.Load(configData, withValidation ? GetValidationSchemas() : null);
					// ReSharper restore ExpressionIsAlwaysNull
					// ReSharper restore ConditionIsAlwaysTrueOrFalse
					return configuration;
				}
			}
		}

		/// <summary>
		/// Merges the supplied <paramref name="template"/> with the supplied <paramref name="velocityContext"/> using 
		/// the NVelocity engine
		/// </summary>
		/// <param name="template">the template to merge</param>
		/// <param name="velocityContext">the variables to merge into the template</param>
		/// <returns>the merged version of the template</returns>
		protected virtual string Merge(Template template, IContext velocityContext)
		{
			string output;
			using (var writer = new StringWriter())
			{
				template.Merge(velocityContext, writer);
				output = writer.GetStringBuilder().ToString();
			}
			return output;
		}

		/// <summary>
		/// Loads the NVelocity template specified
		/// </summary>
		/// <param name="templateFile">the file representing the template</param>
		/// <param name="encoding">the encoding to read the file with</param>
		/// <returns>the loaded template</returns>
		protected virtual Template GetTemplate(FileInfo templateFile, string encoding)
		{
			// ReSharper disable PossibleNullReferenceException
			var vEngine = _velocityEngineFactory.Invoke(templateFile.Directory.FullName);
			// ReSharper restore PossibleNullReferenceException
			return vEngine.GetTemplate(templateFile.Name, encoding);
		}

		/// <summary>
		/// Constructs a <see cref="Contract.Configuration.Configuration"/> from an existing directory
		/// </summary>
		/// <param name="root">The directory to start from</param>
		/// <param name="targetSystem">The backend-store to use for member-lookup</param>
		/// <param name="recursive">Wether to process just the <paramref name="root"/> directory, or also it's children</param>
		/// <param name="onAfterVisit">Optional action to call after visiting a directory and building it's configuration</param>
		/// <param name="onVisitError">Optional action to call when an error during visiting a directory occurs</param>
		/// <returns>the successfully built configuration</returns>
		/// <exception cref="ArgumentException">If the <paramref name="root"/> directory does not exist</exception>
		public virtual Contract.Configuration.Configuration BuildFromDirectory(string root, string targetSystem, bool recursive
			, Action<ConfigurationExtensions.VisitingContext> onAfterVisit, Action<ConfigurationExtensions.VisitingError> onVisitError)
		{
			var adapter = _storageAdapterFactory.Invoke(targetSystem);
		  var strategy = _configurationConstructorStrategyFactory.Invoke(targetSystem);
      var config = strategy.BuildFromDirectory(root, adapter, recursive, onAfterVisit, onVisitError);
			return config;
		}

		/// <summary>
		/// Returns all registered schemas to be used for validating the serialized <see cref="Configuration"/>
		/// </summary>
		/// <returns>A list of all schemas</returns>
		protected virtual XmlSchemaSet GetValidationSchemas()
		{
			var set = new XmlSchemaSet();
			foreach (var schema in _validationSchemas)
				set.Add(schema);
			return set;
		}

		#endregion
	}
}